1. 前言
1.1 说明
【说明:部分原创 & 整合了多篇网络文章,链接附在文末。】
AOP(Aspect Orient Programming),作为面向对象编程的一种补充,广泛应用于处理一些具有横切性质的系统级服务,如事务管理、安全检查、缓存、对象池管理等。
AOP 实现的关键就在于 AOP 框架自动创建的 AOP 代理,AOP 代理则可分为静态代理和动态代理两大类,其中静态代理是指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强;而动态代理则在运行时借助于 JDK 动态代理、CGLIB 等在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。
2. AspectJ
2.1 AspectJ 背景
AspectJ 是 eclipse 基金会的一个项目,官网就在 eclipse 官网里。官网里提供了一个aspectJ.jar的下载链接,但其实这个链接只是一个安装包,把安装包里的东西解压后就是一个文档 + 脚本 + jar包的程序包。
2.2 AspectJ 工具文件组织
1 | myths@pc:~/aspectj1.8$ tree bin/ lib/ |
这当中重点的文件是四个jar包中的前三个,bin文件夹中的脚本其实都是调用这些jar包的命令。
- aspectjrt.jar 包主要是提供运行时的一些注解,静态方法等等东西,通常我们要使用aspectJ的时候都要使用这个包。
- aspectjtools.jar 包主要是提供赫赫有名的ajc编译器,可以在编译期将将java文件或者class文件或者aspect文件定义的切面织入到业务代码中。通常这个东西会被封装进各种IDE插件或者自动化插件中。
- aspectjweaverjar 包主要是提供了一个java agent用于在类加载期间织入切面(Load time weaving,LTW)。并且提供了对切面语法的相关处理等基础方法,供ajc使用或者供第三方开发使用。这个包一般我们不需要显式引用,除非需要使用 LTW。
上面的说明其实也就指出了aspectJ的几种标准的使用方法(参考文档):
- 编译时织入,利用ajc编译器替代javac编译器,直接将源文件(java或者aspect文件)编译成class文件并将切面织入进代码。
- 编译后织入,利用ajc编译器向javac编译期编译后的class文件或jar文件织入切面代码。
- 加载时织入,不使用ajc编译器,利用aspectjweaver.jar工具,使用java agent代理在类加载期将切面织入进代码。
2.3 下载 AspectJ
- weaver:https://repo1.maven.org/maven2/org/aspectj/aspectjweaver/1.8.14/aspectjweaver-1.8.14.jar
- jrt:https://repo1.maven.org/maven2/org/aspectj/aspectjrt/1.8.14/aspectjrt-1.8.14.jarhttps://mvnrepository.com/artifact/org.aspectj/aspectjweaver/1.8.14https://repo1.maven.org/maven2/org/aspectj/aspectjrt/1.8.14/aspectjrt-1.8.14.jar
- tools: https://repo1.maven.org/maven2/org/aspectj/aspectjtools/1.8.14/aspectjtools-1.8.14.jar
2.4 AspectJ DEMO
2.4.1 文件目录结构
1 | - com.yupaopao.aspectj |
2.4.2 源代码
1 | public class AspectJMain { |
2.4.3 执行
- 编译:(也可以直接使用 ajc -d . ./src )
1 | java -jar aspectjtools-1.8.14.jar -cp aspectjrt-1.8.14.jar -sourceroots ./src -d . |
- 运行:java -cp aspectjrt-1.8.14.jar:. AspectJMain
1 | AspectJMain is running... |
- decopmile:
https://github.com/java-decompiler/jd-gui/releases/download/v1.6.6/jd-gui-1.6.6.jar
1 | java -jar jd-gui-1.6.6.jar |
可以看到 class 文件如下:
1 | public class AspectJMain { |
虽然事实上这种基于aj文件的切面描述方法比基于java注解的切面描述方法用起来要灵活的多,但是由于他无法摆脱ajc的支持,而且本身不兼容java语法导致难以统一编码规范,加上需要较多额外的学习成本,因此事实上很多项目还是不怎么用这种方式,更多的还是采用了兼容java语法的用注解定义切面的方式。
2.5. 基于 java 注解的 AspectJ
下面我们主要还是着力考虑下基于java注解的切面使用方法。
2.5.1 准备
先建一个普通的项目看看,老样子,从maven的maven-archetype-quickstart开始,pom.xml,pom文件里我们一般只需要加上aspetjrt的依赖即可。:
1 | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
创建App.java文件:
1 | public class App { |
创建切面类AnnoAspect.java:
1 |
|
1 | 当前项目结构应该是这样的: |
其实就是创建了一个对App类进行切面的AnnoAspect类,这个类需要加上@Aspect注解用以声明这是一个切面,以及其他相关切面语法。接下来我们就来尝试下三种不同的编译方式。
2.5.2 编译时织入
编译时织入其实就是使用ajc来进行编译,暂时不使用自动化构建工具,我们先在项目根目录下手动写一个编译脚本compile.sh:
1 |
|
调用aspectjtools.jar,在-cp里指明aspectjrt.jar的路径,-source 1.5指明支持java1.5以后的注解,-sourceroots指明编译的文件夹,-d指明输出路径。
这样就会生成AnnoAspect.class和App.class两个文件。AnnoAspect.class:
1 |
|
App.class
1 | public class App |
我们发现ajc对AnnoAspect的处理方法与跟AjAspect的处理方法类似,都是将类声明成单例,并且识别AspectJ语法,将相关函数织入到App中。运行(在项目根目录执行):
1 | $ java -cp ~/.m2/repository/org/aspectj/aspectjrt/1.8.9/aspectjrt-1.8.9.jar:src/main/java/ com.mythsman.test.App |
2.5.3 编译后织入
编译后织入其实就是在javac编译完成后,用ajc再去处理class文件得到新的、织入过切面的class文件。仍然是上面的项目,我们先用javac编译一下:
1 | $ javac -cp ~/.m2/repository/org/aspectj/aspectjrt/1.8.9/aspectjrt-1.8.9.jar -d target/classes src/main/java/com/mythsman/test/*.java |
编译成功后生成了AnnoAspect.class以及App.class。显然,这两个class文件反编译后还是源文件的样子,并没有什么用,因此这时候执行App的main函数发现切面并没有生效。因此我们仍然需要用ajc来处理:
1 | !/usr/bin/env bash |
这样就把target/classes中原来的class文件替换成了织入后的class文件。反编译之后发现与采用编译期织入方法的结果基本相同。
2.5.4 加载时织入(LTW)
前两种织入方法都依赖于ajc的编译工具,LTW却通过java agent机制在内存中操作类文件,可以不需要ajc的支持做到动态织入。不过,这里有一个挺有意思的问题,我们知道编译期一定会编译AnnoAspect类,那么这时候通过切面语法我们就可以找到他要处理的App类,这大概就是编译阶段织入的大概流程。但是如果在类加载期处理的话,当类加载到App类的时候,我们并不知道这个类需要被AnnoAspect处理。
因此为了实现LTW,我们肯定要有个配置文件,来告诉类加载器,某某某切面需要优先考虑,他们很可能会影响其他的类。为了实现LTW,我们需要在资源目录下配置META-INF/aop.xml文件,来告知类加载器我们当前注册的切面。在上面的项目中,我们其实只需要创建src/main/resources/META-INF/aop.xml:
1 | <aspectj> |
这样,我们就可以先使用javac编译源文件,再使用java agent在运行时织入:
1 |
|
运行结果:
1 | AnnoAspect before say |
当然,如果可以使用ajc的话,我们也可以通过-outxml参数来自动生成xml文件。
3. JDK 动态代理
JDK自从1.3版本开始,就引入了动态代理,并且经常被用来动态地创建代理。JDK的动态代理用起来非常简单,唯一限制便是使用动态代理的对象必须实现一个或多个接口。而CGLIB缺不必有此限制。
代理类具有以下属性:
- 如果所有代理接口都是公共的,代理类是公共的、最终的,而不是抽象的。
- 如果任何代理接口是非公共的,则代理类是非公共的、最终的,并且不是抽象的。
- 以”$Proxy” 字符串开头的类名空间应该保留给代理类。
- 代理类以相同的顺序实现在其创建时指定的接口。
- 如果代理类实现了非公共接口,那么它将与该接口定义在同一个包中。否则,代理类的包也是未指定的。
- 由于代理类实现了在其创建时指定的所有接口,调用getInterfaces其 Class对象将返回一个包含相同接口列表的数组(按照其创建时指定的顺序),调用 getMethods其Class对象将返回一个Method对象数组,其中包括这些接口中的所有方法,调用getMethod将按预期在代理接口中找到方法。
- 每个代理类都有一个公共构造函数,它接受一个参数,即 interface 的一个实现InvocationHandler,以设置代理实例的调用处理程序。
当代理类的两个或多个接口包含具有相同名称和参数签名的方法时,代理类的接口的顺序就变得很重要。当在代理实例上调用重复方法时,Method包含代理类接口列表中的方法(直接或通过超接口继承)的最前面接口中方法的对象被传递给调用处理程序的invoke方法,而不管方法调用通过何种引用类型发生。
如果代理接口包含具有相同的名称和参数签名的方法hashCode,equals或toString,当这种方法在代理实例调用的 Method对象传递到调用处理程序将 java.lang.Object其声明类。换句话说,public && ! final方法在java.lang.Object 逻辑上位于所有代理接口之前,用于确定将哪个Method对象传递给调用处理程序。
注意 clone 方法默认是 protect 权限的,这意味着 proxy 对象的 clone 方法不能被访问。如果 proxy 实现了定义 Object clone(); 方法的接口,这个 clone() 需要开发人员自行实现,而不会调用到 Object#clone() 。
proxy 仍然是可以使用 synchronized 加锁的对象, wait/notify 方法是可用的。
另请注意,当重复方法被分派到调用处理程序时,如果 invoke方法抛出一个不可分配的异常类型(没有对应异常的接口方法),则 unchecked 异常会被包上 UndeclaredThrowableException。
3.1 基本使用
代理一般实现的模式为JDK静态代理:创建一个接口,然后创建被代理的类实现该接口并且实现该接口中的抽象方法。之后再创建一个代理类,同时使其也实现这个接口。在代理类中持有一个被代理对象的引用,而后在代理类方法中调用该对象的方法。
1 | public class JavaProxy { |
运行结果:
1 | invoker triggered |
注意 proxy1.hashCode() 获取到的哈希值实际上是 echoHandler 中 obj 的 hashCode,所以 proxy1 与 proxy2 在使用同一个 echoHandler 时 hashCode 相同,但是 == 地址判断返回 false。
其中接口数组的长度可以是 0,但是 invokeHandler 不可以为 null。
JDK动态代理是基于接口实现的。因为通过接口指向实现类实例的多态方式,可以有效地将具体实现与调用解耦,便于后期的修改和维护。
3.3 jdk 动态代理原理
3.3.1 InvocationHandler
在动态代理中,核心功能实现就是InvocationHandler。每一个代理的实例都会有一个关联的调用处理程序(InvocationHandler)。对待代理实例进行调用时,将对方法的调用进行编码并指派到它的调用处理器(InvocationHandler)的invoke方法。所以对代理对象实例方法的调用都是通过InvocationHandler中的invoke方法来完成的,而invoke方法会根据传入的代理对象、方法名称以及参数决定调用代理的哪个方法。
3.3.2 newProxyInstance
当我们需要构建一个动态代理对象时,调用了 Proxy 的静态方法:
1 | public static Object newProxyInstance(ClassLoader loader, |
该方法通过代理对象实现的接口,从指定 classloader 中,获得实现这些接口的动态代理类的类对象,其中构造方法存在一个类型为 InvocationHandler 的参数,即直接通过 h 就可以生成动态代理对象。
Proxy 类中使用 ClassLoaderValue 存储已经生成的代理类的 Constructor<?>。同时该 cache 还传入了 proxy 类对象的工厂类,属于懒加载的 LoadingCache:
1 | private static final WeakCache<ClassLoader, Class<?>[], Class<?>> proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory()); |
其映射关系就是 classLoader -> { interfaces -> proxyClass , … }
3.3.3 ProxyClassFactory
当 proxyClassCache 中找不到目标 proxy 的类的类对象时,就会调用 实现了 BiFunction<ClassLoader, Class<?>[], Class<?>> 接口的 ProxyClassFactory 的 apply 方法来生成 proxy 类对象(jdk 11 使用的 ProxyBuilder 来构建):
1 | private static final class ProxyClassFactory |
其中注释相当详细,概括其中步骤如下:
- 从指定 classloader 中获得传参待实现的接口数组每一个接口的 class
- 如果地址不一致,说明要实现的接口的类不在传入的 classLoader 中,抛出异常
IllegalArgumentException( intf + " is not visible from class loader")
- 如果非接口类,抛出
IllegalArgumentException( interfaceClass.getName() + " is not an interface")
- 如果重复,抛出
IllegalArgumentException("repeated interface: " + interfaceClass.getName())
- 如果地址不一致,说明要实现的接口的类不在传入的 classLoader 中,抛出异常
- 检查待实现的接口权限是否 public
- 如过不是,检查是多个接口是否同包(如果只有 0-1 个则没问题),否则抛出
IllegalArgumentException( "non-public interfaces from different packages")
- 注意接口中只要有一个非 public 的接口, proxy 就会成为 非 public 的类
- proxy 类一定是 final 的。
- 如过不是,检查是多个接口是否同包(如果只有 0-1 个则没问题),否则抛出
- 确定 proxy 的包路径与名称
- 包路径默认与第一个接口同包,否则使用
PROXY_PACKAGE_PREFIX = "com.sun.proxy"
包 - 注意 jdk 9 之后还存在 Module 模块,代理类的包名有一定变动
- 代理类名称为
"$Proxy" + 【原子long递增】
- 包路径默认与第一个接口同包,否则使用
其中 defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length) 调用 jvm 向 classLoader 载入了 class 对象。看到 0 结尾就猜测到这是一个 native 方法:
1 | private static native Class<?> defineClass0(ClassLoader loader, String name, byte[] b, int off, int len); |
3.3.4 generateClassFile() 生成代理类的二进制 class
proxyClassFile 由 ProxyGenerator.generateProxyClass( proxyName, interfaces, accessFlags)
生成,其中关键方法为 generateClassFile()
:
1 | private byte[] generateClassFile() { |
其中注释非常详细,概述如下:
- 加入 Object 与所有接口的所有方法到 proxyMethods
- addProxyMethod 先加入 Object 的 hashCodeMethod、equalsMethod、toStringMethod 方法,覆盖即多态
- 遍历并加入待实现的每一个接口的每一个方法 addProxyMethod(m, intf)
- checkReturnTypes 确认签名重复的方法的返回值一致,否则抛出 IllegalArgumentException( “methods with same signature “ + getFriendlyMethodSignature(pm.methodName, pm.parameterTypes) + “ but incompatible return types: “ + newReturnType.getName() + “ and others”);
- 组装所有方法到 methods
- methods.add(generateConstructor()) 加入构造方法,参数为 InvocationHandler
- 加入所有 proxyMethod 的静态私有字段(未初始化)
- 加入所有 proxyMethod 的方法(generateMethod() ),其中调用了 InvocationHandler 字段的 invoke 方法(即 toString、hashCode 与 equals 最后调用的 InvocationHandler 的 incoke 来取结果)
- methods.add(generateStaticInitializer()) 生成 static 代码块,填充 所有 proxyMethod 的静态私有字段
- 检查方法与字段数量,都不能超过 65535(2^16)
- 输出 final class 文件到二进制流,按 class 的定义规范进行写入,最终转换成 byte 数组
4. CGLIB 与 ASM
CGLIB(Code Generation Library)它是一个代码生成类库。它可以在运行时候动态是生成某个类的子类。代理模式为要访问的目标对象提供了一种途径,当访问对象时,它引入了一个间接的层。
CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM(Java字节码操控框架),来转换字节码并生成新的类。
CGLIB代理主要通过对字节码的操作,为对象引入间接级别,以控制对象的访问。JDK动态代理虽然简单易用,但是只能对接口进行代理。如果要代理的类为一个普通类、没有接口,那么Java动态代理就没法使用了。
需要注意的是,CGLib不能对声明为final的方法进行代理,因为CGLib原理是动态生成被代理类的子类。对于从Object中继承的方法,CGLIB代理也会进行代理,如hashCode()、equals()、toString()等,但是getClass()、wait()等方法不会,因为它是final方法,CGLIB无法代理。
4.1 CGLIB
CGLIB 原理:动态生成一个要代理类的子类,子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用java反射的JDK动态代理要快。
CGLIB 底层:使用字节码处理框架ASM,来转换字节码并生成新的类。不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。
CGLIB缺点:对于final方法,无法进行代理。
Java安全性通过不受信任的代码保护系统资源免受未经授权的访问。 代码可以由签名者标识,代码基url (jar或类文件)可以在本地或从网络下载。 CGLIB生成的类在配置和JVM启动时不存在(在运行时生成),但是所有生成的类都具有与CGLIB本身相同的保护域(签名者和代码库),可以在WS中使用,也可以在带有安全管理器的RMI应用程序中使用。 授予生成类的权限,授予cglib二进制文件的权限。 默认的安全配置在java中。政策文件。这是示例策略文件,它授予cglib和生成代码的所有权限。
1 | [java] |
(java 安全策略:/Users/dz0400803/.sdkman/candidates/java/8.0.275.hs-adpt/jre/lib/security/)
详情移步:
4.1.1 Enhancer 与 Callback
Enhancer 类是 CGLib 中的一个字节码增强器,它可以方便的对你想要处理的类进行扩展。
在CGLib回调时可以设置对不同方法执行不同的回调逻辑,或者根本不执行回调。在JDK动态代理中并没有类似的功能,对InvocationHandler接口方法的调用对代理类内的所以方法都有效。
最简单的 Enhancher 可以直接使用静态方法实现:
1 | public static class Bean { |
输出:
1 | enhancer.intercept |
等价于:
1 | Enhancer eh = new Enhancer(); |
如果想对不同方法使用不同回调,则可以配合 setCallbacks 与 setCallbackFilter 一起使用。其中 setCallbacks 设置了一个回调数组,setCallbackFilter 配置了 method -> 【回调数组下标】 的映射方法。
1 | public class TargetObj { |
输出结果为:
1 | com.yupaopao.cglib.TargetObj$$EnhancerByCGLIB$$2c89b5d0@1225439493 |
注意,在上面的示例中,任何方法调用都将被委托,也将调用java.lang.Object中定义的方法。
查看 Callback 接口,可以看到共有以下子接口:
1 | /** |
4.1.2 懒(延时)加载
LazyLoader 与 Dispatcher 接口继承了 Callback,因此也算是 CGLib 中的一种 Callback 类型。
Dispatcher 和 LazyLoader 的区别在于:LazyLoader 只在第一次访问延迟加载属性时触发代理类回调方法,而 Dispatcher 在每次访问延迟加载属性时都会触发代理类回调方法。
1 | public class LoaderMain { |
输出结果:
1 | lazyProp=lazy count=1,dispatcherProp=dispatcher count=1,lCount=1,dCount=1 |
4.1.3 InterfaceMaker
InterfaceMaker 会动态生成一个接口,该接口包含指定类定义的所有方法。
1 | public class InterfaceMain { |
输出结果:
1 | absM |
可以看到,不论是父类还是接口,只要是 public 的方法都会抽取出来,而 protected/private 的方法不会。另外,其自身的 public final 会被抽出,但父类的 public final 不会。
4.1.4 常见问题
4.1.4.1 StackOverflowError
使用 InvocationHandler 时会对所有调用都将使用相同的 InvocationHandler 进行分派,因此可能导致无休止的循环:
1 | Bean enhancer = (Bean) Enhancer.create(Bean.class, new InvocationHandler() { |
为了避免这种情况,我们可以使用另一个回调分派器: MethodInterceptor
。但是在使用中,需要注意必须使用 invokesuperer
方法来调用超类方法。如果超方法是抽象的,它将抛出 AbstractMethodError
:
1 | Object intercept(Object proxy, Method method, |
4.1.4.2 OutOfMemoryError:PermSize Space 内存溢出
当增强器创建一个类时,它将为每个拦截器设置一个 private static 字段,其初始值为空。使用 cglib 创建的类定义在创建后不能重用(不能直接 new 它生成的 class ,而是需要借助 Enhancer 的 create() 方法),因为注册回调不会成为生成类初始化阶段的一部分,而是在类已经被JVM初始化之后由 cglib 手工准备。 MethodInterceptor 将会触发额外的类的创建并在增强的类中注册额外的字段:
1 | private static final ThreadLocal CGLIB$THREAD_CALLBACKS; |
因此回调变量存储在增强器的缓存中,因此使用 Enhancer 创建大量实例时,推荐使用同一个 Enhancher 调用 create() 方法,而不是创建大量 Enhancher。另一种方式是维护 Enhancher 中注册的 Callback 单例子。还有一种方式就是重写 hashCode 与 equals 方法来使用缓存。调用 enhancher.setUseCache(true); 可以控制启用与否(默认为 true)。
4.2 ASM
对于需要手动操纵字节码的需求,可以使用ASM,它可以直接生产 .class字节码文件,也可以在类被加载入JVM之前动态修改类行为。ASM的应用场景有AOP(Cglib就是基于ASM)、热部署、修改其他jar包中的类等。当然,涉及到如此底层的步骤,实现起来也比较麻烦。
为了更快地理解ASM的处理流程,需要先了解访问者模式。简单来说,访问者模式主要用于修改或操作一些数据结构比较稳定的数据。
4.2.1 ASM 核心API
ASM Core API可以类比解析XML文件中的SAX方式,不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件。好处是非常节约内存,但是编程难度较大。然而出于性能考虑,一般情况下编程都使用Core API。在Core API中有以下几个关键类:
- ClassReader:用于读取已经编译好的.class文件。
- ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件。
- 各种Visitor类:如上所述,CoreAPI根据字节码从上到下依次处理,对于字节码文件中不同的区域有不同的Visitor,比如用于访问方法的MethodVisitor、用于访问类变量的FieldVisitor、用于访问注解的AnnotationVisitor等。为了实现AOP,重点要使用的是MethodVisitor。
ASM Tree API可以类比解析XML文件中的DOM方式,把整个类的结构读取到内存中,缺点是消耗内存多,但是编程比较简单。TreeApi不同于CoreAPI,TreeAPI通过各种Node类来映射字节码的各个区域,类比DOM节点,就可以很好地理解这种编程方式。
4.2.2 ASM AOP DEMO
1 | public class Base { |
生成的 class 文件:
1 | public class Base { |
4.3 Spring AOP
Spring AOP也是对目标类增强,生成代理类。但是与AspectJ的最大区别在于—Spring AOP的运行时增强,而AspectJ是编译时增强。
做一个简单的实验我们就可以发现,如果我们使用spring aop来对某一个service进行切面处理,那么调用getClass()方法获得的结果就是:
1 | Myservice$$EnhancerBySpringCGLIB$$3afc9148 |
显然,虽然spring aop采用了aspectj语法来定义切面,但是在实现切面逻辑的时候还是采用CGLIB来进行动态代理的方法。
值得注意的是,动态代理的类不能再通过 new 对象的方式产生 aop 效果,而应该从代理容器中获取对象。
4.3.1 execution 语法
格式:
1 | execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?) |
- 拦截任意公共方法 execution(public (..))
- 拦截以set开头的任意方法 execution( set(..))
- 拦截类或者接口中的方法 execution( com.xyz.service.AccountService.(..))
- 拦截包或者子包中定义的方法 execution( com.xyz.service...*(..))
4.3.2 spring aop 原理概述
用到的包:aspectjweaver 与 spring-aop
Spring提供了两种方式来生成代理对象: JDKProxy和Cglib,具体使用哪种方式生成由AopProxyFactory根据AdvisedSupport对象的配置来决定。默认的策略是如果目标类是接口,则使用JDK动态代理技术,否则使用Cglib来生成代理。
cglib 获取代理对象的 proxy 工厂在 CglibAopProxy 中,其原理通过 Enhancer 新建的代理对象;JDK 生成代理对象的工厂则是 JdkDynamicAopProxy ,其中 getProxy(classLoader) 获取代理类的方法如下:
1 |
|
JdkDynamicAopProxy 本身实现了 InvokationHandler 接口,在 Proxy.newProxyInstance(classLoader, proxiedInterfaces, this) 处将自己传为处理器。其中主流程可以简述为:获取可以应用到此方法上的通知链(Interceptor Chain),如果有则应用通知并执行joinpoint;;如果没有则直接反射执行 joinpoint。更细节的实现不再展开。
7 小结
- AspectJ
- 三个包:
- aspectjrt.jar 包主要是提供运行时的一些注解,静态方法等
- aspectjtools.jar 包主要是 ajc 编译器
- aspectjweaverjar 包主要是提供了一个 java agent 用于在类加载期间织入切面(Load time weaving,LTW)
- aspect 关键字与其他语法
- 三种织入方式
- 编译时织入:ajc 编译
- 编译后织入:javac 编译完成后,用ajc再去处理class文件得到新的、织入过切面的class文件
- 加载时织入(LTW):通过 java agent 机制在内存中操作类文件
- 三个包:
- JDK 动态代理
- Proxy.newProxyInstance 三个参数:类加载器,接口数组,InvocationHandler
- 不代理 final 方法,无法代理实现类,只能代理接口
- jdk 动态代理原理
- WeakCache(jdk 8,11 为 ClassLoaderValue) 存储已经生成的代理类
- ProxyClassFactory 生成代理类的类对象(jdk 11 使用的 ProxyBuilder 来构建),构造方法的唯一参数为 InvocationHandler
- generateClassFile() 生成代理类的二进制 class,先加入 Object 的 hashCodeMethod、equalsMethod、toStringMethod 方法
- 小心递归调用导致栈溢出
- 注意重写 hashCode 与 equals 方法
- CGLIB
- 原理:动态生成一个要代理类的子类,子类重写要代理的类的所有不是final的方法
- 六类 Callback
- MethodInterceptor 方法拦截
- NoOp 直接调用 super 方法
- LazyLoader 延时加载:加载一次
- Dispatcher 延时加载:每次更新
- InvocationHandler invoke 拦截
- FixedValue 定值,小心出现 ClassCastException
- 常见问题
- StackOverflowError:Callback 里循环调用增强类的方法,触发了 Callback 自身,导致循环调用
- OutOfMemoryError:Callback 最好使用单例
- ASM
- 它可以直接生产 .class字节码文件,也可以在类被加载入JVM之前动态修改类行为
- 使用基于访问者模式
- Core API:类比解析 XML 文件的 SAX 方式,不需要把这个类的整个结构读取进来
- Tree API:类比解析 XML 文件的 DOM 方式,缺点是消耗内存多,但是编程比较简单
- ClassReader / ClassWrite
- Visitor类:MethodVisitor、FieldVisitor、AnnotationVisitor 等,重点要使用的是 MethodVisitor
- Spring AOP
- 包:aspectjweaver 与 spring-aop,注解使用 aspectjweaver,实现使用两种方式来生成代理对象: JDKProxy 和 Cglib
- cglib 获取代理对象的 proxy 工厂在 CglibAopProxy 中,其原理通过 Enhancer 新建的代理对象
- JDK 生成代理对象的工厂则是 JdkDynamicAopProxy ,本身实现了 InvokationHandler 接口
- 包:aspectjweaver 与 spring-aop,注解使用 aspectjweaver,实现使用两种方式来生成代理对象: JDKProxy 和 Cglib
8 参考
- 面试官:说说Spring AOP、AspectJ、CGLIB ?它们有什么关系?
- 原生AspectJ用法分析以及Spring-AOP原理分析
- 深入理解JDK动态代理机制
- CGLIB(Code Generation Library) 介绍与原理
- Class Proxy - java doc
- CGLIB-WIKI
- 【译】Cglib缺失的文档
- Java Proxy和CGLIB动态代理原理
- 简单分析cglib引起的PermSize Space内存溢出
- ASM
- Java ASM系列
- 字节码增强技术探索
- Aspect Oriented Programming with Spring
- Spring AOP原理分析一次看懂